###############################################################################
#1.  Book Classes
###############################################################################
'''
Write the Book class so that it passes testBookClass, and uses the OOP constructs we learned this week as appropriate.
'''

class Book(object) :
    pass

def testBookClass():
    print("Testing Book class...", end="")
    # A Book has a title, and author, and a number of pages.
    # It also has a current page, which always starts at 1. There is no page 0!
    book1 = Book("Harry Potter and the Sorcerer's Stone", "J. K. Rowling", 309)
    assert(str(book1) == "Book<Harry Potter and the Sorcerer's Stone by " +
                 "J. K. Rowling: 309 pages, currently on page 1>")
    book2 = Book("Carnegie Mellon Motto", "Andrew Carnegie", 1)
    assert(str(book2) == "Book<Carnegie Mellon Motto by Andrew Carnegie: " +
                 "1 page, currently on page 1>")

    # You can turn pages in a book. Turning a positive number of pages moves
    # forward; turning a negative number moves backwards. You can't move past
    # the first page going backwards or the last page going forwards
    book1.turnPage(4) # turning pages does not return
    assert(book1.getCurrentPage() == 5)
    book1.turnPage(-1)
    assert(book1.getCurrentPage() == 4)
    book1.turnPage(400)
    assert(book1.getCurrentPage() == 309)
    assert(str(book1) == "Book<Harry Potter and the Sorcerer's Stone by " +
                         "J. K. Rowling: 309 pages, currently on page 309>")
    book2.turnPage(-1)
    assert(book2.getCurrentPage() == 1)
    book2.turnPage(1)
    assert(book2.getCurrentPage() == 1)

    # You can also put a bookmark on the current page. This lets you turn
    # back to it easily. The book starts out without a bookmark.
    book3 = Book("The Name of the Wind", "Patrick Rothfuss", 662)
    assert(str(book3) == "Book<The Name of the Wind by Patrick Rothfuss: " + \
                         "662 pages, currently on page 1>")
    assert(book3.getBookmarkedPage() == None)
    book3.turnPage(9)
    book3.placeBookmark() # does not return
    assert(book3.getBookmarkedPage() == 10)
    book3.turnPage(7)
    assert(book3.getBookmarkedPage() == 10)
    assert(book3.getCurrentPage() == 17)
    assert(str(book3) == "Book<The Name of the Wind by Patrick Rothfuss: " + \
                         "662 pages, currently on page 17, page 10 bookmarked>")
    book3.turnToBookmark()
    assert(book3.getCurrentPage() == 10)
    book3.removeBookmark()
    assert(book3.getBookmarkedPage() == None)
    book3.turnPage(25)
    assert(book3.getCurrentPage() == 35)
    book3.turnToBookmark() # if there's no bookmark, don't turn to a page
    assert(book3.getCurrentPage() == 35)
    assert(str(book3) == "Book<The Name of the Wind by Patrick Rothfuss: " + \
                         "662 pages, currently on page 35>")

    # Finally, you should be able to compare two books directly
    book5 = Book("A Game of Thrones", "George R.R. Martin", 807)
    book6 = Book("A Game of Thrones", "George R.R. Martin", 807)
    book7 = Book("A Natural History of Dragons", "Marie Brennan", 334)
    book8 = Book("A Game of Spoofs", "George R.R. Martin", 807)
    assert(book5 == book6)
    assert(book5 != book7)
    assert(book5 != book8)
    book5.turnPage(1)
    assert(book5 != book6)
    book5.turnPage(-1)
    assert(book5 == book6)
    book6.placeBookmark()
    assert(book5 != book6)
    print("Done!")



###############################################################################
#2. Student Classes
###############################################################################
'''
Define class(es) so that testStudentOOP will pass! Make sure to use inheritance properly!
'''

class Student(object):
    pass


def testStudentOOP():
    print("Testing Student Classes...", end="")
    chaya = Student("Chaya", "Comp Bio")
    chaya.addClass("76-100")

    kyle = OneTwelveStudent("Kyle", "IS", "JJ")
    kyle.addClass("76-100")

    assert(chaya.classes == set(['76-100']))
    assert(kyle.classes == set(['15-112', '76-100']))

    arman = Student("Arman", "ECE")
    kdchin = OneTwelveStudent("Kyle", "IS", "DD")

    assert(chaya != arman)
    assert(kyle != kdchin)
    assert(type(arman) == Student)
    assert(type(kyle) == OneTwelveStudent)
    assert(isinstance(kyle, Student))

    x = 5
    try:
        chaya.get112Grade()
    except:
        x = 4
    assert(x == 4)
    assert(kyle.get112Grade() == 42)
    kyle.study112()
    assert(kyle.get112Grade() == 42 + 42)

    assert(sorted(str(Student.students)) == sorted(
        "{Student named Chaya, OneTwelveStudent named Kyle, Student named Arman, OneTwelveStudent named Kyle}"))

    assert(sorted(str(OneTwelveStudent.students)) == sorted(
        "{OneTwelveStudent named Kyle, OneTwelveStudent named Kyle}"))

    assert(OneTwelveStudent.getSchool() == "Carnegie Mellon")
    print("Passed!")

###############################################################################
# 3. Gate Classes
###############################################################################
'''
A logic gate is a physical device that creates the functional equivalent of a logic operation in code[ and, or, not].  
It takes some number of input values, such as two values for and (input1 and input2), and produces a single output 
value.Write the Gate classes required to make the following test function work properly.
'''


def getLocalMethods(clss):
    import types
    # This is a helper function for the test function below.
    # It returns a sorted list of the names of the methods
    # defined in a class.
    result = []
    for var in clss.__dict__:
        val = clss.__dict__[var]
        if (isinstance(val, types.FunctionType)):
            result.append(var)
    return sorted(result)

def testGateClasses():
    print("Testing Gate Classes... ", end="")

    # require methods be written in appropriate classes
    assert (getLocalMethods(Gate) == ['__init__', '__str__',
                                      'numberOfInputs', 'setInput'])
    assert (getLocalMethods(AndGate) == ['getOutput'])
    assert (getLocalMethods(OrGate) == ['getOutput'])
    assert (getLocalMethods(NotGate) == ['getOutput', 'numberOfInputs'])

    # make a simple And gate
    and1 = AndGate()
    assert (type(and1) == AndGate)
    assert (isinstance(and1, Gate) == True)
    assert (and1.numberOfInputs() == 2)
    and1.setInput(0, True)
    and1.setInput(1, False)
    # Hint: to get the name of the class given an object obj,
    # you can do this:  type(obj).__name__
    # You might do this in the Gate.__str__ method...
    assert (str(and1) == "And(True,False)")
    assert (and1.getOutput() == False)
    and1.setInput(1, True)  # now both inputs are True
    assert (and1.getOutput() == True)
    assert (str(and1) == "And(True,True)")

    # make a simple Or gate
    or1 = OrGate()
    assert (type(or1) == OrGate)
    assert (isinstance(or1, Gate) == True)
    assert (or1.numberOfInputs() == 2)
    or1.setInput(0, False)
    or1.setInput(1, False)
    assert (or1.getOutput() == False)
    assert (str(or1) == "Or(False,False)")
    or1.setInput(1, True)
    assert (or1.getOutput() == True)
    assert (str(or1) == "Or(False,True)")

    # make a simple Not gate
    not1 = NotGate()
    assert (type(not1) == NotGate)
    assert (isinstance(not1, Gate) == True)
    assert (not1.numberOfInputs() == 1)
    not1.setInput(0, False)
    assert (not1.getOutput() == True)
    assert (str(not1) == "Not(False)")
    not1.setInput(0, True)
    assert (not1.getOutput() == False)
    assert (str(not1) == "Not(True)")

    print("Passed!")

testGateClasses()

###############################################################################
# 4. Building Class
###############################################################################
'''
Write the classes Building and School so that they pass the following test cases. You may not hardcode any test cases. 
    You must use inheritance appropriately.
'''
def testBuildingClass():
    # A building is either open or closed. It starts out open.
    b = Building()
    assert(str(b) == "Building(Closed=False)")
    # A building can be closed and opened
    b.close()
    assert(str(b) == "Building(Closed=True)")
    b.open()
    assert(str(b) == "Building(Closed=False)")
    # A School is a Building that has students.
    s = School(2)
    assert(str(s) == "School(Students=2,Closed=False)")
    # You can't close a school if there are students inside
    ok = False
    try:
        s.close()
    except:
        ok = True
    assert(ok)
    # You can remove students, but the number of students inside can't go below 0.
    s.removeStudent()
    assert(str(s) == "School(Students=1,Closed=False)")
    s.removeStudent()
    assert(str(s) == "School(Students=0,Closed=False)")
    s.removeStudent()
    assert(str(s) == "School(Students=0,Closed=False)")
    # Once there are no students, then you can close the school.
    s.close()
    assert(str(s) == "School(Students=0,Closed=True)")
    # Check various things about inheritance
    assert(isinstance(b, Building) == True)
    assert(isinstance(b, School) == False)
    assert(isinstance(s, Building) == True)
    assert(isinstance(s, School) == True)
    # Buildings don't have students....
    ok = False
    try:
        b.removeStudent()
    except:
        ok = True
    assert(ok)

###############################################################################
# 5. Stapler Class
###############################################################################
'''
 Write the classes Stapler and ElectricStapler so that the following test code runs without errors. Do not harcode against
 the values used in teh testcases, though you can assume the testcases cover the needed functionality. You must
 use proper object-oriented design, including goog inheritance.
'''
def testStaplerClass():
    # A stapler starts out full of the specified number of staples
    s = Stapler(50)
    assert(str(s) == "Stapler(50/50)")
    # You can staple something if the stapler has staples
    s.staple()
    assert(str(s) == "Stapler(49/50)")
    # You can refill the stapler to get it back to full. Full is the number of staples it started with, which
    # could be different # from stapler to stapler.
    s.refill()
    assert(str(s) == "Stapler(50/50)")
    for i in range(50):
        s.staple()
    assert(str(s) == "Stapler(0/50)")
    # If you try to staple while empty, nothing changes
    s.staple()
    assert(str(s) == "Stapler(0/50)")
    # An electric stapler always starts with 500 staples, and is unplugged
    e = ElectricStapler()
    assert(str(e) == "DeadStapler(500/500)")
    # You can plugin an electric stapler
    e.plugIn()
    assert(str(e) == "ElectricStapler(500/500)")
    # An electric stapler can also staple and refill
    e.staple()
    assert(str(e) == "ElectricStapler(499/500)")
    e.refill()
    assert(str(e) == "ElectricStapler(500/500)")
    # But an unplugged electric stapler can't staple, you get a "No Power" exception
    e.unPlug()
    ok = False
    try:
        e.staple()
    except:
        ok = True
    assert(ok)
    # Like a normal stapler, an electric stapler also doesn't change if you staple while empty
    e.plugIn()
    for i in range(500):
        e.staple()
    assert(str(e) == "ElectricStapler(0/500)")
    e.staple()
    assert(str(e) == "ElectricStapler(0/500)")
    # Checking inheritance
    assert(isinstance(s, Stapler) == True)
    assert(isinstance(s, ElectricStapler) == False)
    assert(isinstance(e, Stapler) == True)
    assert(isinstance(e, ElectricStapler) == True)


